package com.google.code.mochaccino.framework.rest.resources;
/*
 * Copyright 2012 Claude Houle claude.houle@gmail.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sun.net.www.protocol.file.FileURLConnection;

/** A Resource Loader Implementation based on URLs. */
public class ResourceLoaderImpl implements ResourceLoader {

	/** The Base URLs */
	private URL[] baseURLs = null;

	/** The read-write lock for refresh. */
	private ReentrantReadWriteLock lock = new ReentrantReadWriteLock( false );

	/** Logger */
	private Logger logger = LoggerFactory.getLogger( getClass() );

	/** Resources Map */
	private Map<String,URL> resources = new TreeMap<String,URL>();

	/** Temp Directory */
	private File tempDir;

	/** Constructor */
	public ResourceLoaderImpl( Resource... resources ) {
		this( new File( System.getProperty( "java.io.tmpdir" ) ), resources );
	}

	/** Constructor */
	public ResourceLoaderImpl( File tempDir, Resource... resources ) {
		this.baseURLs = getURLs( resources );
		this.tempDir = tempDir;
		refresh();
	}

	/** Return the URLs for the resources */
	private URL[] getURLs( Resource[] resources ) {
		List<URL> urlList = new ArrayList<URL>();
		if ( resources != null ) {
			for ( Resource r : resources ) {
				for ( URL u : r.getURLs() ) {
					urlList.add( u );
				}
			}
		}
		return urlList.toArray( new URL[urlList.size()] );
	}

	/** Return the URL for specified resource id. The resource id is relative to one of the base-path */
	@Override
	public URL getResource( String s ) {
		Lock myLock = null;
		try {
			myLock = lock.readLock();
			myLock.lock();
			return resources.get( s );
		} finally {
			if ( myLock != null ) {
				myLock.unlock();
			}
		}
	}

	/** Initialize Resources URLs based on the Base URLs. */
	@Override
	public void refresh() {
		Lock myLock = null;
		try {
			myLock = lock.writeLock();
			myLock.lock();
			resources.clear();
			for ( int i = 0; i < baseURLs.length; i++ ) {
				URL url = baseURLs[i];
				if ( url.toExternalForm().startsWith( "jar:" ) ) {
					url = unjar( url );
				}

				File file = new File( url.getFile() );
				try {
					lookup( url, file );
				} catch ( IOException e ) {
					logger.error( e.getMessage(), e );
				}
			}
		} catch ( IOException e ) {
			throw new RuntimeException( e );
		} finally {
			if ( myLock != null ) {
				myLock.unlock();
			}
		}
	}

	/** Lookup specified File */
	private void lookup( URL baseURL, File file ) throws IOException {
		if ( file.isDirectory() ) {
			for ( File child : file.listFiles() ) {
				lookup( baseURL, child );
			}
		} else {
			URL fileURL = file.toURL();
			String fileURLExternalForm = fileURL.toExternalForm();
			String baseURLExternalForm = baseURL.toExternalForm();
			if ( fileURLExternalForm.startsWith( baseURLExternalForm ) ) {
				String path = fileURLExternalForm.substring( baseURLExternalForm.length() );
				if ( path != null && path.length() > 0 ) {
					if ( resources.containsKey( path ) ) {
						logger.warn( "Resource Id '{}' is already registered to resource '{}'. Skipping path '{}'",
						             new Object[] { path,
						                            resources.get( path ).toExternalForm(),
						                            fileURLExternalForm } );
					} else {
						resources.put( path, fileURL );
					}

					if ( resources.containsKey( "/" + path ) ) {
						logger.warn( "Resource Id '{}' is already registered to resource '{}'. Skipping path '{}'",
						             new Object[] { "/" + path,
						                            resources.get( path ).toExternalForm(),
						                            fileURLExternalForm } );
					} else {
						resources.put( "/" + path, fileURL );
					}
				}
			}
		}
	}

	/** Unjar resources from specified jar-entry into the temporary directory. */
	private URL unjar( URL baseURL ) throws IOException {
		String baseUrlExternalForm = baseURL.toExternalForm();
		String jarUrlExternalForm = baseUrlExternalForm.substring( 4, baseUrlExternalForm.lastIndexOf( "!" ) );
		String entryPath = baseUrlExternalForm.substring( baseUrlExternalForm.lastIndexOf( "!" ) + 2 );

		URL jarURL = new URL( jarUrlExternalForm );
		FileURLConnection conn = (FileURLConnection) jarURL.openConnection();
		JarInputStream jarIn = null;

		try {
			jarIn = new JarInputStream( conn.getInputStream() );
			JarEntry entry = null;
			while ( (entry = jarIn.getNextJarEntry()) != null ) {
				if ( entry.getName().startsWith( entryPath ) ) {
					String entryName = entry.getName();
					File entryFile = new File( tempDir, entryName );
					if ( entry.isDirectory() ) {
						if ( !entryFile.mkdirs() ) {
							logger.error( "Failed to create dir path:" + entryFile.getAbsolutePath() );
						}
					} else {
						BufferedOutputStream out = null;
						try {
							out = new BufferedOutputStream( new FileOutputStream( entryFile ) );
							IOUtils.copy( jarIn, out );
						} finally {
							IOUtils.closeQuietly( out );
						}
					}
				}
			}
		} finally {
			IOUtils.closeQuietly( jarIn );
		}
		return new File( tempDir, entryPath ).toURI().toURL();
	}

	/** To String Implementation. */
	public String toString() {
		StringBuffer buf = new StringBuffer();
		for ( Map.Entry<String,URL> url : resources.entrySet() ) {
			buf.append( url.getKey() );
			buf.append( " -> " );
			buf.append( url.getValue().toExternalForm() );
			buf.append( System.getProperty( "line.separator" ) );
		}
		return buf.toString();
	}
}